'''
Spine JSON to Maya Converter

By: A. Baker

Created: 11/2014

About: Converts a Spine generated JSON into readable data in Maya.
    It then uses the data to recreate Spine skeletons and animations
    in Maya.
    
Install: Use this link to figure out where to place this file.
    http://docs.autodesk.com/MAYAUL/2014/ENU/Maya-API-Documentation/files/GUID-FA51BD26-86F3-4F41-9486-2C3CF52B9E17.htm
    
    Typically it will go here: \Users\<username>\Documents\maya
    
Usage: Copy the following lines and run them in Maya's Python Script Editor:

    import maya.cmds as cmds
    cmds.loadPlugin('SpineToMaya.py')
    cmds.SpineToMaya()
'''

'''
============================================================
--->>   IMPORT MODULES
============================================================
'''
import os
import sys
import math
import json
import traceback

from PySide import QtGui
from PySide import QtCore
from shiboken import wrapInstance

import maya.OpenMaya as OpenMaya
import maya.OpenMayaAnim as OpenMayaAnim
import maya.OpenMayaMPx as OpenMayaMPx
import maya.OpenMayaUI as OpenMayaUI


'''
============================================================
--->>   GLOBALS
============================================================
'''
kPluginCmdName = 'SpineToMaya'


'''
============================================================
--->>   PLUG-IN CLASS
============================================================
'''
class MyCommandClass( OpenMayaMPx.MPxCommand ):
    
    def __init__( self ):
        '''
        Constructor.
        '''
        OpenMayaMPx.MPxCommand.__init__( self )
    
    
    def doIt( self, args ):
        '''
        Command execution.
        '''
        try:
            browser.deleteLater()
        except:
            pass
    
        browser = BatcherWindow( parent=maya_main_window() )
        
        # Delete the UI if errors occur to avoid causing winEvent
        # and event errors (in Maya 2014)
        try:
            browser.show()
        except:
            browser.deleteLater()
            traceback.print_exc()


'''
============================================================
--->>   JSON HANDLER CLASSES
============================================================
'''
class SpineJSON( object ):
    '''
    Class for manipulating JSON files.
    '''
    
    def __init__( self, json_filepath=None ):
        '''
        @param json_filepath: String. Full path to a Spine JSON file.
        '''
        # Stores the JSON file path.
        self.filepath = json_filepath
        
        # Stores the bones from the JSON. Dict key is the name of the bone.
        # Dict value is the spine_json_bone class.
        self.bones = {}
        self.bone_build_order = None
        
        # Stores the animation from a JSON.
        self.animation = {}
        
        # Get the file data.
        self.readFile()


    def readFile( self ):
        '''
        Reads the JSON file.
        '''
        # Open and load the data.
        json_open = open( self.filepath )
        json_data = json.load( json_open )
        
        for key in json_data:
            print key
            
        bone_data = json_data['bones'] # This is a list.
        skin_data = json_data['skins']
        for bone_name in skin_data.keys():
            skin_bone_data = skin_data[bone_name]
            print bone_name
            print '    1. {0}'.format( skin_bone_data )
            
            for mesh_name in skin_bone_data.keys():
                mesh_data = skin_bone_data[mesh_name]
                print '    2. {0}'.format( mesh_name )
                #print '        3. {0}'.format( mesh_data )
                
                for mesh_key in mesh_data.keys():
                    mesh_properties = mesh_data[mesh_key]
                    print '        3. {0}'.format( mesh_key )
                    
                    for key_4 in mesh_properties.keys():
                        print '            4. {0}: {1}'.format( key_4, mesh_properties[key_4] )
                
                
        
        
        # Organize the bones into a build order starting at the root.
        self.bone_build_order = self.buildOrder( bone_data )
        
        for bone_name in self.bone_build_order:
            # Get the bones. This is a list.
            for bone in bone_data:
                if bone['name'] == bone_name:
                    # There is no guarantee that a bone entry in the JSON will
                    # have all the bone attributes.
                    new_bone = SpineBone()
                    
                    for bone_attr in bone.keys():
                        if bone_attr == 'name':
                            # The names from Spine have spaces between words. Maya doesn't
                            # allow this and will automatically replace whitespace with an underscore.
                            new_bone.name = str( bone['name'] ).replace( ' ', '_' )
                    
                        elif bone_attr == 'parent':
                            new_bone.parent = str( bone['parent'] ).replace( ' ', '_' )
                    
                        elif bone_attr == 'length':
                            new_bone.length = float( bone['length'] )
                        
                        elif bone_attr == 'x':
                            new_bone.x = float( bone['x'] )
                    
                        elif bone_attr == 'y':
                            new_bone.y = float( bone['y'] )
                            
                        elif bone_attr == 'scaleX':
                            # Force all base scales to be 1,1,1.
                            #new_bone.scaleX = float( bone['scaleX'] )
                            new_bone.scaleX = 1.0
                            
                        elif bone_attr == 'scaleY':
                            #new_bone.scaleY = float( bone['scaleY'] )
                            new_bone.scaleY = 1.0
                            
                        elif bone_attr == 'rotation':
                            new_bone.rotation = float( bone['rotation'] )
                            
                        else:
                            print 'ROGUE BONE ATTR: {0}'.format( bone_attr )
                            
                    # Build a world matrix for this bone.
                    matrix = OpenMaya.MMatrix()
                    bone_mtrans = OpenMaya.MTransformationMatrix( matrix )
                    
                    # Translation.
                    # Defaults.
                    tx = OpenMaya.MDistance( 0.0, OpenMaya.MDistance.kCentimeters )
                    ty = OpenMaya.MDistance( 0.0, OpenMaya.MDistance.kCentimeters )
                    tz = OpenMaya.MDistance( 0.0, OpenMaya.MDistance.kCentimeters )
                    
                    if new_bone.x:
                        tx = OpenMaya.MDistance( new_bone.x, OpenMaya.MDistance.kCentimeters )
                        
                    if new_bone.y:
                        ty = OpenMaya.MDistance( new_bone.y, OpenMaya.MDistance.kCentimeters )
                        
                    tran_vector = OpenMaya.MVector( tx.value(), ty.value(), tz.value() )
                    bone_mtrans.setTranslation( tran_vector, OpenMaya.MSpace.kWorld )
                                        
                    # Rotation.
                    # Defaults
                    rx = math.radians( 0.0 )
                    ry = math.radians( 0.0 )
                    rz = math.radians( 0.0 )
                    
                    if new_bone.rotation:
                        rz = math.radians( new_bone.rotation )

                    rot_util = OpenMaya.MScriptUtil()
                    rot_util.createFromDouble( rx, ry, rz )
                    bone_mtrans.setRotation( rot_util.asDoublePtr(), OpenMaya.MSpace.kWorld )
                    
                    # Scale.
                    # Defaults.
                    sx = 1
                    sy = 1
                    sz = 1
                    
                    if new_bone.scaleX:
                        sx = new_bone.scaleX
                        
                    if new_bone.scaleY:
                        sy = new_bone.scaleY
                    
                    scale_util = OpenMaya.MScriptUtil()
                    scale_util.createFromDouble( sx, sy, sz )
                    bone_mtrans.setScale( scale_util.asDoublePtr(), OpenMaya.MSpace.kWorld )
                    
                    new_bone.local_matrix = bone_mtrans.asMatrix()                     
                                            
                    if new_bone.parent == None:
                        new_bone.world_matrix = bone_mtrans.asMatrix()
                        new_bone.tran_matrix = bone_mtrans.asMatrix()
                        
                    else:
                        # Get the parent object's matrix.
                        parent_matrix = self.bones[new_bone.parent].tran_matrix
                        #parent_matrix = self.bones[new_bone.parent].local_matrix
                        parent_mtrans = OpenMaya.MTransformationMatrix( parent_matrix )
                        
                        # Set the scale of the parent matrix to that of the bone's.
                        scale_util = OpenMaya.MScriptUtil()
                        scale_util.createFromDouble( sx, sy, sz )
                        parent_mtrans.setScale( scale_util.asDoublePtr(), OpenMaya.MSpace.kWorld )
                        
                        # Get the bone's matrix as a world matrix.
                        final_matrix = bone_mtrans.asMatrix() * parent_mtrans.asMatrix()
                        #final_matrix = new_bone.local_matrix * parent_mtrans.asMatrix()
                        final_mtrans = OpenMaya.MTransformationMatrix( final_matrix )
                        #self.printTransforms('trans matrix', final_mtrans)
                        new_bone.tran_matrix = final_mtrans.asMatrix()
                        
                        
                        # Set the bone's rotation back to the JSON value.
                        rot_matrix = OpenMaya.MMatrix()
                        rot_mtrans = OpenMaya.MTransformationMatrix( rot_matrix )
                        rot_util = OpenMaya.MScriptUtil()
                        rot_util.createFromDouble( rx, ry, rz )
                        #print rx, ry, OpenMaya.MAngle( rz ).asDegrees()
                        rot_mtrans.setRotation( rot_util.asDoublePtr(), OpenMaya.MSpace.kTransform )
                        #self.printTransforms('rot matrix', rot_mtrans)
                        
                        rot_mtrans.setTranslation( final_mtrans.getTranslation( OpenMaya.MSpace.kTransform ), OpenMaya.MSpace.kTransform )
                        
                        #self.printTransforms('rot matrix', rot_mtrans)
              
                        # Set the bone class property.
                        new_bone.world_matrix = rot_mtrans.asMatrix()
                    
                    # Add the bone class instance to the bone property.
                    self.bones[ new_bone.name ] = new_bone
                    
        # Get the animations. This is a dict.
        LOOP_BREAK = 0
        json_anim_data = json_data['animations']

        for anim_key in json_anim_data.keys():
            if LOOP_BREAK == 1:
                break
            else:
                new_anim = SpineAnimation()
                
                new_anim.name = anim_key
                
                json_anim_bones_data = json_anim_data[anim_key]['bones']
                for bone_name in json_anim_bones_data.keys():
                    # Convert bone name to a string.
                    json_bone_name = str( bone_name )
                    maya_bone_name = str( bone_name ).replace( ' ', '_' )
                    
                    # Create a bone timeline
                    new_anim.bones[maya_bone_name] = SpineBoneTimeline()
                    new_anim.bones[maya_bone_name].name = maya_bone_name
                    
                    # Loop through the timeline types: translate, scale and rotate.
                    for timeline_type in json_anim_bones_data[json_bone_name].keys():
                        # Get the dictionary of data for the current timeline type.
                        timeline_dict = json_anim_bones_data[json_bone_name][timeline_type]

                        # Loop through each key for this timeline item.
                        for timeline_data in timeline_dict:
                            # Loop through each attribute for the timeline item.
                            bone_timeline_list = SpineBoneTimeline.AnimTimeline()
                            
                            for timeline_attr in timeline_data.keys():
                                if timeline_attr == 'curve':
                                    bone_timeline_list.curve = timeline_data[timeline_attr]
                                    
                                elif timeline_attr == 'time':
                                    bone_timeline_list.time = timeline_data[timeline_attr]
                                
                                elif timeline_attr == 'x':
                                    bone_timeline_list.x = timeline_data[timeline_attr]
                                
                                elif timeline_attr == 'y':
                                    bone_timeline_list.y = timeline_data[timeline_attr]
                                
                                elif timeline_attr == 'angle':
                                    bone_timeline_list.angle = timeline_data[timeline_attr]
                                
                                else:
                                    print '        ROGUE TIMELINE ATTR: {0}'.format( key )
                                    
                            # Add the bone timeline list to the bone timeline object.
                            new_anim.bones[maya_bone_name].timeline[timeline_type].append( bone_timeline_list )
                                    
                # Add the animation to the class property.
                self.animation[anim_key] = new_anim
            
        # Close the JSON file.
        json_open.close()

        
    def buildOrder( self, bone_data ):
        '''
        Puts the bones in the order they need to be built.
        
        @param bone_data: Spine JSON data for bones.
        @return: List of Unicode bone names.
        '''
        bones = []
        for bone in bone_data:
            bones.append( bone['name'] )
            
        build_order = []
        while bones:
            for bone_idx, bone_name in enumerate( bones ):
                for bone in bone_data:
                    if bone_name == bone['name']:
                        try:
                            parent = bone['parent']
                        except:
                            parent = None
                
                        if parent == None:
                            build_order.append( bone_name )
                            bones.pop( bone_idx )
                            
                        elif parent in build_order:
                            build_order.append( bone_name )
                            bones.pop( bone_idx )
                    
        return build_order
    
    
    def printTransforms( self, name, mtrans_matrix ):
        print '    {0}'.format( name )
        
        trans = mtrans_matrix.getTranslation( OpenMaya.MSpace.kWorld )
        print '        m t: {0},{1},{2}'.format( trans.x, trans.y, trans.z )
        
        rot = mtrans_matrix.eulerRotation()
        print '        m r: {0},{1},{2}'.format( OpenMaya.MAngle( rot.x ).asDegrees(),
                                             OpenMaya.MAngle( rot.y ).asDegrees(),
                                             OpenMaya.MAngle( rot.z ).asDegrees() )
        
        util = OpenMaya.MScriptUtil()
        util.createFromDouble(0.0, 0.0, 0.0)
        ptr = util.asDoublePtr()
        mtrans_matrix.getScale( ptr, OpenMaya.MSpace.kWorld )
        scale = [OpenMaya.MScriptUtil().getDoubleArrayItem( ptr, 0 ),
                 OpenMaya.MScriptUtil().getDoubleArrayItem( ptr, 1 ),
                 OpenMaya.MScriptUtil().getDoubleArrayItem( ptr, 2 )]
        print '        m s: {0},{1},{2}'.format( scale[0], scale[1], scale[2] )


class SpineBone( object ):
    '''
    Stores a Spine bone's data.
    '''
    def __init__( self ):
        self.name = None # string
        self.parent = None # string
        self.length = None # float
        self.x = None # float
        self.y = None # float
        self.scaleX = None # float
        self.scaleY = None # float
        self.rotation = None # float
        self.tran_matrix = None
        self.local_matrix = None
        self.world_matrix = None


class SpineAnimation( object ):
    '''
    Stores a Spine animation's data.
    '''
    def __init__( self ):
        self.name = None
        self.bones = {}


class SpineBoneTimeline( object ):
    '''
    Stores a Spine bone timeline's data.
    '''
    class AnimTimeline():
        '''
        Class for storing a bone's timeline data.
        '''
        def __init__( self ):
            self.time = None # Float, Time in seconds.
            self.curve = None # Can be 'linear', 'stepped' or a bezier as a list [cx1,cy1,cx2,cy2]
            self.x = None # Float. Local space. Relative to setup pose.
            self.y = None # Float. Local space. Relative to setup pose.
            self.angle = None # Float. Local space. Relative to setup pose.
    
    def __init__( self ):
        self.name = None # String. Name of bone.        
        self.timeline = {}
        self.timeline['translate'] = []
        self.timeline['scale'] = []
        self.timeline['rotate'] = []
        
        
'''
============================================================
--->>   UTILITY SCRIPTS
============================================================
'''
def getMObject( obj_name ):
    '''
    Converts a Maya object string name into a MObject.
    
    @param obj_name: String. Name of a Maya object.
    @return: MObject or None.
    '''
    try:
        sel_list = OpenMaya.MSelectionList()
        obj = OpenMaya.MObject()
        sel_list.add( obj_name )
        sel_list.getDependNode( 0, obj )
        return obj
    except:
        return None


def getMDagPath( obj_name ):
    '''
    Converts a Maya object string name into a MDagPath.
    
    @param obj_name: String. Name of a Maya object.
    @return: MDagPath or None.
    '''
    try:
        sel_list = OpenMaya.MSelectionList()
        sel_list.add( obj_name )
        dag = OpenMaya.MDagPath()
        component = OpenMaya.MObject()
        sel_list.getDagPath( 0, dag, component )    
        return dag
    except:
        return None
    '''
    except IOError as e:
        print e.errno, e.strerror
    except:
        print sys.exc_info()[0]
    '''
   
    
def getMPlugByName( obj, plug_name ):
    '''
    Get an object's attribute by name.
    
    @param obj: MObject. MObject with the attribute.
    @param plug_name: String. Name of the attribute.
    @return: MPlug or None.
    '''
    dep_fn = OpenMaya.MFnDependencyNode( obj )
    
    try:
        return dep_fn.findPlug( plug_name )
    except:
        return None


def objectExists( obj_name ):
    '''
    Checks if the object exists in the scene.
    
    @param obj_name: String. Name of a Maya object.
    @return: Bool.
    '''
    result = False
    if getMObject( obj_name ) is not None:
        result = True
    return result
    

def translateBy( dag_path, mvector, space ):
    '''
    Relatively change the translation component of the MDagPath.
    
    @param dag_path: MDagPath.
    @param mvector: MVector.
    @param space: MSpace.
    '''
    OpenMaya.MFnTransform( dag_path ).translateBy( mvector, space )
    
    
def rotateBy( dag_path, rotation ):
    '''
    Relatively change the rotation component of the MDagPath.
    
    @param dag_path: MDagPath.
    @param rotation: MEulerRotation or MQuaternion.
    '''
    OpenMaya.MFnTransform( dag_path ).rotateBy( rotation )
    

'''
============================================================
--->>   BUILDERS
============================================================
'''
def buildSkeleton( json_data, measurement_unit ):
    '''
    Builds a Spine skeleton from the provided JSON file.
    
    @param json_data: SpineJSON class.
    @param measurement_unit: MDistance Unit. kInches, kFeet, kYards, kMiles,
      kMillimeters, kCentimeters, kKilometers, kMeters.
    '''
    dag_mod = OpenMaya.MDagModifier()
    
    for bone in json_data.bone_build_order:
        bone_name = str( bone ).replace( ' ', '_' )

        # Get the bones. This is a list.
        for json_bone_name in json_data.bones.keys():
            if json_bone_name == bone_name:
                print 'BUILD: {0}'.format( bone_name )
                
                json_bone = json_data.bones[json_bone_name]
                
                if json_bone.parent is not None:
                    parent_obj = getMObject( json_bone.parent )
                    bone_obj = dag_mod.createNode( 'joint', parent_obj )
                else:
                    bone_obj = dag_mod.createNode( 'joint' )
                
                dag_mod.doIt()
                
                # Name it.
                dep_fn = OpenMaya.MFnDependencyNode( bone_obj )
                dep_fn.setName( json_bone.name )
                
                # Transform the bone into it's base pose.
                dag_path = getMDagPath( json_bone.name )
                trans_fn = OpenMaya.MFnTransform( dag_path )
                #trans_fn.set( OpenMaya.MTransformationMatrix( json_bone.world_matrix ) )
                bone_mtrans = OpenMaya.MTransformationMatrix( json_bone.world_matrix )
                trans_fn.setTranslation( bone_mtrans.getTranslation( OpenMaya.MSpace.kWorld ), OpenMaya.MSpace.kWorld )
                trans_fn.setRotation( bone_mtrans.rotation() )
                
                util = OpenMaya.MScriptUtil()
                util.createFromDouble(0.0, 0.0, 0.0)
                ptr = util.asDoublePtr()
                bone_mtrans.getScale( ptr, OpenMaya.MSpace.kWorld )
                scale = [OpenMaya.MScriptUtil().getDoubleArrayItem( ptr, 0 ),
                         OpenMaya.MScriptUtil().getDoubleArrayItem( ptr, 1 ),
                         OpenMaya.MScriptUtil().getDoubleArrayItem( ptr, 2 )]
                
                #scale_util = OpenMaya.MScriptUtil()
                #scale_util.createFromDouble( scale[0], scale[1], scale[2] )
                trans_fn.setScale( ptr )
                

def resetSkeleton( json_data=None, namespace=None ):
    '''
    Resets a Spine skeleton back to its bind pose.
    
    @param json_data: SpineJSON class.
    @param namespace: String. Namespace of the skeleton. Can be None.
    '''        
    bone_data = json_data.bones
    
    for bone_name in json_data.bone_build_order:
        for bone_key in bone_data.keys():            
            if bone_key == bone_name:
                bone_class = bone_data[bone_key]
                bone_name_full = '{0}{1}'.format( namespace, bone_class.name )
                
                dag_path = getMDagPath( bone_name_full )
                world_mtrans = OpenMaya.MTransformationMatrix( bone_data[bone_key].world_matrix )
                trans_fn = OpenMaya.MFnTransform( dag_path )
                trans_fn.setTranslation( world_mtrans.getTranslation( OpenMaya.MSpace.kWorld ), OpenMaya.MSpace.kWorld )
                trans_fn.setRotation( world_mtrans.rotation() )
                
                
def applyAnimation( json_data=None, anim_name=None, measurement_unit=None, namespace=None, do_tangents=False ):
    '''
    Applies an animation to a Spine JSON skeleton.
    
    @param json_data: SpineJSON class.
    @param anim_name: String. Animation name.
    @param measurement_unit: MDistance Unit. kInches, kFeet, kYards, kMiles,
      kMillimeters, kCentimeters, kKilometers, kMeters.
    @param namespace: String. Namespace of the Spine skeleton. Can be None.
    '''
    debug = False
    
    # Get the namespace if there is one.
    if namespace is not None:
        namespace = '{0}:'.format( namespace )
    else:
        namespace = ''

    # Reset the skeleton to its bind pose.
    resetSkeleton( json_data=json_data, namespace=namespace )

    # Get the animation timelines.
    anim_data = json_data.animation[anim_name]

    # Loop through each bone in the animation.
    bone_data = json_data.bones
    
    for bone_name in json_data.bone_build_order:
        for bone_key in anim_data.bones.keys():
            if bone_key == bone_name:# and bone_key == 'LeftLow2':
                # Create the animCurve nodes for the animated attributes.
                bone_name_full = '{0}{1}'.format( namespace, bone_name )
                
                print 'Applying animation to... {0}'.format( bone_name )
                
                dag_path = getMDagPath( bone_name_full )
                
                # Prep for adding animations.
                anim_curve_fn = OpenMayaAnim.MFnAnimCurve()
    
                # Translate plugs..
                x_tran_plug = getMPlugByName( dag_path.node(), 'translateX' )
                x_tran_array = OpenMaya.MObjectArray()
                if not OpenMayaAnim.MAnimUtil.findAnimation( x_tran_plug, x_tran_array ):
                    x_tran_anim_node = anim_curve_fn.create( x_tran_plug )
                else:
                    x_tran_anim_node = x_tran_array[0]            
                
                y_tran_plug = getMPlugByName( dag_path.node(), 'translateY' )
                y_tran_array = OpenMaya.MObjectArray()
                if not OpenMayaAnim.MAnimUtil.findAnimation( y_tran_plug, y_tran_array ):
                    y_tran_anim_node = anim_curve_fn.create( y_tran_plug )
                else:
                    y_tran_anim_node = y_tran_array[0]
                    
                translate_anim_nodes = [x_tran_anim_node,
                                        y_tran_anim_node]
                
                # Rotate plugs..
                z_rot_plug = getMPlugByName( dag_path.node(), 'rotateZ' )
                z_rot_array = OpenMaya.MObjectArray()
                if not OpenMayaAnim.MAnimUtil.findAnimation( z_rot_plug, z_rot_array ):
                    z_rot_anim_node = anim_curve_fn.create( z_rot_plug )
                else:
                    z_rot_anim_node = z_rot_array[0]
                    
                rotate_anim_nodes = [z_rot_anim_node]
                
                # Scale plugs..
                x_scl_plug = getMPlugByName( dag_path.node(), 'scaleX' )
                x_scl_array = OpenMaya.MObjectArray()
                if not OpenMayaAnim.MAnimUtil.findAnimation( x_scl_plug, x_scl_array ):
                    x_scl_anim_node = anim_curve_fn.create( x_scl_plug )
                else:
                    x_scl_anim_node = x_scl_array[0]
                
                y_scl_plug = getMPlugByName( dag_path.node(), 'scaleY' )
                y_scl_array = OpenMaya.MObjectArray()
                if not OpenMayaAnim.MAnimUtil.findAnimation( y_scl_plug, y_scl_array ):
                    y_scl_anim_node = anim_curve_fn.create( y_scl_plug )
                else:
                    y_scl_anim_node = y_scl_array[0]
                    
                scale_anim_nodes = [x_scl_anim_node,y_scl_anim_node]
                    
                # Loop through each bone timeline.
                for timeline_type in anim_data.bones[bone_name].timeline.keys():
                    print '    Applying {0}'.format( timeline_type )
                    
                    # Get the list of AnimTimeline associated with this timeline type.
                    timeline_list = anim_data.bones[bone_name].timeline[timeline_type]
                    
                    # The list is in order from first key to last.
                    # First create all the keys.
                    for keyframe_idx, keyframe in enumerate( timeline_list ):
                        previous_keyframe = timeline_list[keyframe_idx]
                        
                        if keyframe_idx != 0:
                            previous_keyframe = timeline_list[keyframe_idx-1]
                        
                        # Time is in seconds. Convert to uiUnit used in Maya.
                        frame = math.ceil( (keyframe.time * 30) )
                        mtime = OpenMaya.MTime( frame, OpenMaya.MTime.uiUnit() )
                        
                        # Convert the Spine tangent into a Maya tangent.
                        tangent_in = getKeyTangent( previous_keyframe.curve )
                        tangent_out = getKeyTangent( keyframe.curve )
                            
                        if timeline_type == 'translate':
                            if keyframe.x is not None or keyframe.y is not None:
                                # Get the new transform of the joint for this frame.
                                x = OpenMaya.MDistance( keyframe.x, OpenMaya.MDistance.kCentimeters )
                                y = OpenMaya.MDistance( keyframe.y, OpenMaya.MDistance.kCentimeters )
                                mvector = OpenMaya.MVector( x.asUnits( measurement_unit ), y.asUnits( measurement_unit ), 0.0 )
                                bone_mtrans = OpenMaya.MTransformationMatrix( bone_data[bone_name].local_matrix )
                                bone_mtrans.addTranslation( mvector, OpenMaya.MSpace.kTransform )
                                final_x = bone_mtrans.getTranslation( OpenMaya.MSpace.kTransform ).x
                                final_y = bone_mtrans.getTranslation( OpenMaya.MSpace.kTransform ).y
                                
                                # Add the X and Y keyframes.
                                anim_curve_fn.setObject( x_tran_anim_node )
                                anim_curve_fn.setIsWeighted( True )
                                anim_curve_fn.addKeyframe( mtime, final_x, tangent_in, tangent_out )
                                
                                anim_curve_fn.setObject( y_tran_anim_node )
                                anim_curve_fn.setIsWeighted( True )
                                anim_curve_fn.addKeyframe( mtime, final_y, tangent_in, tangent_out )
                                                                       
                        elif timeline_type == 'rotate':
                            if keyframe.angle is not None:
                                # Get the new transform of the joint for this frame.
                                bone_mtrans = OpenMaya.MTransformationMatrix( bone_data[bone_name].local_matrix )
                                rvec = OpenMaya.MVector( 0.0, 0.0, OpenMaya.MAngle( keyframe.angle, OpenMaya.MAngle.kDegrees ).asRadians() )
                                euler = OpenMaya.MEulerRotation( rvec )
                                bone_mtrans.rotateBy( euler, OpenMaya.MSpace.kWorld )
                                rot = bone_mtrans.eulerRotation()
                                final_z = OpenMaya.MAngle( rot.z, OpenMaya.MAngle.kRadians )
                                                                
                                # Add the Z keyframe to the animCurve.
                                for node in rotate_anim_nodes:
                                    anim_curve_fn.setObject( node )
                                    anim_curve_fn.addKeyframe( mtime, final_z.asRadians(), tangent_in, tangent_out )
                                
                                    # Filter the curve to remove gimbal lock.
                                    node_name = OpenMaya.MFnDependencyNode( node ).name()
                                    mel_command = 'filterCurve {0}'.format( node_name )
                                    OpenMaya.MGlobal.executeCommand( mel_command )
                                
                        elif timeline_type == 'scale':
                            if keyframe.x is not None or keyframe.y is not None:
                                bone_mtrans = OpenMaya.MTransformationMatrix( bone_data[bone_name].local_matrix )
                                scale_util = OpenMaya.MScriptUtil()
                                scale_util.createFromDouble( keyframe.x, keyframe.y, 1.0 )
                                bone_mtrans.addScale( scale_util.asDoublePtr(), OpenMaya.MSpace.kWorld )
                                
                                final_util = OpenMaya.MScriptUtil()
                                final_util.createFromDouble(0.0, 0.0, 0.0)
                                ptr = final_util.asDoublePtr()
                                bone_mtrans.getScale( ptr, OpenMaya.MSpace.kWorld )
                                final_scale = [OpenMaya.MScriptUtil().getDoubleArrayItem( ptr, 0 ),
                                               OpenMaya.MScriptUtil().getDoubleArrayItem( ptr, 1 ),
                                               OpenMaya.MScriptUtil().getDoubleArrayItem( ptr, 2 )]
                                
                                anim_curve_fn.setObject( x_scl_anim_node )
                                anim_curve_fn.setIsWeighted( True )
                                anim_curve_fn.addKeyframe( mtime, final_scale[0], tangent_in, tangent_out )
                                
                                anim_curve_fn.setObject( y_scl_anim_node )
                                anim_curve_fn.setIsWeighted( True )
                                anim_curve_fn.addKeyframe( mtime, final_scale[1], tangent_in, tangent_out )
                                
                    # Now handle the key tangents. We do this separate because it relies on
                    # looking at keyframes before and after the key being adjusted.
                    if do_tangents:
                        for keyframe_idx, keyframe in enumerate( timeline_list ):
                            # Time is in seconds. Convert to uiUnit used in Maya.
                            frame = math.ceil( (keyframe.time * 30) )
                            mtime = OpenMaya.MTime( frame, OpenMaya.MTime.uiUnit() )
                            ui_units = OpenMaya.MDistance.uiUnit()
                            
                            previous_keyframe = timeline_list[keyframe_idx]
                            if keyframe_idx != 0:
                                previous_keyframe = timeline_list[keyframe_idx-1]
                            
                            # Convert the Spine tangent into a Maya tangent.
                            tangent_in = getKeyTangent( previous_keyframe.curve )
                            tangent_out = getKeyTangent( keyframe.curve )
                                 
                            if timeline_type == 'translate':                    
                                # If the Spine tangents are bezier then do the conversion and setup here. 
                                if tangent_in == OpenMayaAnim.MFnAnimCurve.kTangentSmooth:
                                    if keyframe_idx != 0:
                                        for node in translate_anim_nodes:
                                            # In tangent to the key (left side of key). This
                                            # value is taken from the previous key's entry.
                                            anim_curve_fn.setObject( node )
                                                                                                                        
                                            # Get the values for this key and the next.
                                            cur_key_value = OpenMaya.MDistance( anim_curve_fn.value( keyframe_idx ), OpenMaya.MDistance.kCentimeters )
                                            prev_key_value = OpenMaya.MDistance( anim_curve_fn.value( keyframe_idx - 1 ), OpenMaya.MDistance.kCentimeters )
                                            
                                            diff_value = abs( prev_key_value.asUnits( ui_units ) - cur_key_value.asUnits( ui_units ) )
                                                                                        
                                            # Get the percentage of the difference.
                                            diff_perc = diff_value * ( 1 - previous_keyframe.curve[3] )
                                            
                                            # Create the tangent angle.
                                            angle = OpenMaya.MAngle( diff_perc, OpenMaya.MAngle.kDegrees )
        
                                            # Get the MTimes for this key and the next.
                                            cur_key_time = anim_curve_fn.time( keyframe_idx )
                                            prev_key_time = anim_curve_fn.time( keyframe_idx - 1 )
                                            
                                            # Get the difference in times.
                                            diff_time = abs( (prev_key_time.value() - cur_key_time.value()) + 1 )
                                            
                                            # Create the tangent weight.
                                            x = (diff_time * previous_keyframe.curve[2])
                                            weight = math.sqrt( angle.asRadians()*angle.asRadians() + x*x )                                           
                                            
                                            # Set the tangent.
                                            anim_curve_fn.setTangentsLocked( keyframe_idx, False )
                                            anim_curve_fn.setWeightsLocked( keyframe_idx, False )
                                            anim_curve_fn.setTangent( keyframe_idx, angle, weight, True )
                                            
                                            if debug:
                                                print '++++++ IN TANGENT'
                                                print '    keyframe: {0}'.format( keyframe_idx )
                                                print '    diff_value: {0}'.format( diff_value )
                                                print '        cur_key_value: {0}'.format( cur_key_value.asUnits( ui_units ) )
                                                print '        prev_key_value: {0}'.format( prev_key_value.asUnits( ui_units ) )
                                                print '    diff_perc: {0}'.format( diff_perc )
                                                print '    angle: {0}'.format( angle.asDegrees() )
                                                print '    current key time: {0}'.format( cur_key_time.value() )
                                                print '    previous key time: {0}'.format( prev_key_time.value() )
                                                print '    diff_time: {0}'.format( diff_time )
                                                print '    x: {0}'.format( x )
                                                print '    weight: {0}'.format( weight )
                                                                                
                                if tangent_out == OpenMayaAnim.MFnAnimCurve.kTangentSmooth:
                                    if keyframe_idx != len(timeline_list)-1:
                                        # Out tangent to the key (right side of the key).
                                        # This value is taken from this key's entry.
                                        for node in translate_anim_nodes:
                                            anim_curve_fn.setObject( node )
                                            
                                            # Get the values for this key and the next.
                                            cur_key_value = OpenMaya.MDistance( anim_curve_fn.value( keyframe_idx ), OpenMaya.MDistance.kCentimeters )
                                            next_key_value = OpenMaya.MDistance( anim_curve_fn.value( keyframe_idx + 1 ), OpenMaya.MDistance.kCentimeters )
                                            diff_value = abs( next_key_value.asUnits( ui_units ) - cur_key_value.asUnits( ui_units ) )
                                            
                                            # Get the percentage of the difference.
                                            diff_perc = diff_value * keyframe.curve[0]
                                            
                                            # Create the tangent angle.
                                            angle = OpenMaya.MAngle( diff_perc, OpenMaya.MAngle.kDegrees )
        
                                            # Get the MTimes for this key and the next.
                                            cur_key_time = anim_curve_fn.time( keyframe_idx )
                                            next_key_time = anim_curve_fn.time( keyframe_idx + 1 )
                                            
                                            # Get the difference in times.
                                            diff_time = abs( (next_key_time.value() - cur_key_time.value()) + 1 )
                                            
                                            # Create the tangent weight.
                                            x = (diff_time * keyframe.curve[1])
                                            weight = math.sqrt( angle.asRadians()*angle.asRadians() + x*x )
                                            
                                            # Set the tangent.                                    
                                            anim_curve_fn.setTangentsLocked( keyframe_idx, False )
                                            anim_curve_fn.setWeightsLocked( keyframe_idx, False )
                                            anim_curve_fn.setTangent( keyframe_idx, angle, weight, False )
                                            
                                            if debug:
                                                print '++++++ OUT TANGENT'
                                                print '    keyframe: {0}'.format( keyframe_idx )
                                                print '    diff_value: {0}'.format( diff_value )
                                                print '        cur_key_value: {0}'.format( cur_key_value.asUnits( ui_units ) )
                                                print '        next_key_value: {0}'.format( next_key_value.asUnits( ui_units ) )
                                                print '    diff_perc: {0}'.format( diff_perc )
                                                print '    angle: {0}'.format( angle.asDegrees() )
                                                print '    current key time: {0}'.format( cur_key_time.value() )
                                                print '    next key time: {0}'.format( next_key_time.value() )
                                                print '    diff_time: {0}'.format( diff_time )
                                                print '    x: {0}'.format( x )
                                                print '    weight: {0}'.format( weight )
    
                            elif timeline_type == 'rotate':                                
                                if tangent_in == OpenMayaAnim.MFnAnimCurve.kTangentSmooth:
                                    if keyframe_idx != 0:
                                        # In tangent to the key (left side of key). This
                                        # value is taken from the previous key's entry.
                                        for node in rotate_anim_nodes:
                                            anim_curve_fn.setObject( node )
                                                                  
                                            # Get the values for this key and the next.
                                            cur_key_value = OpenMaya.MAngle( anim_curve_fn.value( keyframe_idx ), OpenMaya.MAngle.kRadians )
                                            prev_key_value = OpenMaya.MAngle( anim_curve_fn.value( keyframe_idx - 1 ), OpenMaya.MAngle.kRadians )
                                            diff_value = abs( prev_key_value.asDegrees() - cur_key_value.asDegrees() )
                                            
                                            # Get the percentage of the difference.
                                            # For the in tangents a value 1 is to be considered 0.
                                            diff_perc = diff_value * ( 1 - previous_keyframe.curve[3] )
                                            
                                            # Create the tangent angle.
                                            angle = OpenMaya.MAngle( diff_perc, OpenMaya.MAngle.kDegrees )
        
                                            # Get the MTimes for this key and the next.
                                            cur_key_time = anim_curve_fn.time( keyframe_idx )
                                            prev_key_time = anim_curve_fn.time( keyframe_idx - 1 )
                                            
                                            # Get the difference in times.
                                            diff_time = abs( (prev_key_time.value() - cur_key_time.value()) + 1 )
                                            
                                            # Create the tangent weight.
                                            x = (diff_time * previous_keyframe.curve[2])
                                            weight = math.sqrt( angle.asRadians()*angle.asRadians() + x*x )
                                            
                                            # Set the tangent.                                    
                                            anim_curve_fn.setTangentsLocked( keyframe_idx, False )
                                            anim_curve_fn.setWeightsLocked( keyframe_idx, False )
                                            anim_curve_fn.setTangent( keyframe_idx, angle, weight, True )
                                            
                                            if debug:
                                                print '++++++ IN TANGENT'
                                                print '    keyframe: {0}'.format( keyframe_idx )
                                                print '    diff_value: {0}'.format( diff_value )
                                                print '        cur_key_value: {0}'.format( cur_key_value.asDegrees() )
                                                print '        prev_key_value: {0}'.format( prev_key_value.asDegrees() )
                                                print '    diff_perc: {0}'.format( diff_perc )
                                                print '        {0}'.format( previous_keyframe.curve[3] )
                                                print '    angle: {0}'.format( angle.asDegrees() )
                                                print '    current key time: {0}'.format( cur_key_time.value() )
                                                print '    previous key time: {0}'.format( prev_key_time.value() )
                                                print '    diff_time: {0}'.format( diff_time )
                                                print '    x: {0}'.format( x )
                                                print '    weight: {0}'.format( weight )
                                                                                
                                if tangent_out == OpenMayaAnim.MFnAnimCurve.kTangentSmooth:
                                    if keyframe_idx != len(timeline_list)-1:
                                        # Out tangent to the key (right side of the key).
                                        # This value is taken from this key's entry.
                                        for node in rotate_anim_nodes:
                                            anim_curve_fn.setObject( node )
                                            
                                            # Get the values for this key and the next.
                                            cur_key_value = OpenMaya.MAngle( anim_curve_fn.value( keyframe_idx ), OpenMaya.MAngle.kRadians )
                                            next_key_value = OpenMaya.MAngle( anim_curve_fn.value( keyframe_idx + 1 ), OpenMaya.MAngle.kRadians )
                                            diff_value = abs( next_key_value.asDegrees() - cur_key_value.asDegrees() )
                                            
                                            # Get the percentage of the difference.
                                            diff_perc = diff_value * keyframe.curve[0]
                                            
                                            # Create the tangent angle.
                                            angle = OpenMaya.MAngle( diff_perc, OpenMaya.MAngle.kDegrees )
        
                                            # Get the MTimes for this key and the next.
                                            cur_key_time = anim_curve_fn.time( keyframe_idx )
                                            next_key_time = anim_curve_fn.time( keyframe_idx + 1 )
                                            
                                            # Get the difference in times.
                                            diff_time = abs( (next_key_time.value() - cur_key_time.value()) + 1 )
                                            
                                            # Create the tangent weight.
                                            x = (diff_time * keyframe.curve[1])
                                            weight = math.sqrt( angle.asRadians()*angle.asRadians() + x*x )
                                            
                                            # Set the tangent.
                                            anim_curve_fn.setTangentsLocked( keyframe_idx, False )
                                            anim_curve_fn.setWeightsLocked( keyframe_idx, False )
                                            anim_curve_fn.setTangent( keyframe_idx, angle, weight, False )
                                            
                                            if debug:
                                                print '++++++ OUT TANGENT'
                                                print '    keyframe: {0}'.format( keyframe_idx )
                                                print '    diff_value: {0}'.format( diff_value )
                                                print '        cur_key_value: {0}'.format( cur_key_value.asDegrees() )
                                                print '        next_key_value: {0}'.format( next_key_value.asDegrees() )
                                                print '    diff_perc: {0}'.format( diff_perc )
                                                print '        {0}'.format( keyframe.curve[0] )
                                                print '    angle: {0}'.format( angle.asDegrees() )
                                                print '    current key time: {0}'.format( cur_key_time.value() )
                                                print '    next key time: {0}'.format( next_key_time.value() )
                                                print '    diff_time: {0}'.format( diff_time )
                                                print '    x: {0}'.format( x )
                                                print '    weight: {0}'.format( weight )
                                            
                            elif timeline_type == 'scale':
                                if tangent_in == OpenMayaAnim.MFnAnimCurve.kTangentSmooth:
                                    if keyframe_idx != 0:
                                        # In tangent to the key (left side of key). This
                                        # value is taken from the previous key's entry.
                                        for node in scale_anim_nodes:
                                            anim_curve_fn.setObject( node )
                                            
                                            # Get the values for this key and the next.
                                            cur_key_value = anim_curve_fn.value( keyframe_idx )
                                            prev_key_value = anim_curve_fn.value( keyframe_idx - 1 )
                                            diff_value = abs( prev_key_value - cur_key_value )
                                            
                                            # Get the percentage of the difference.
                                            diff_perc = diff_value * ( 1 - previous_keyframe.curve[3] )
                                            
                                            # Create the tangent angle.
                                            angle = OpenMaya.MAngle( diff_perc, OpenMaya.MAngle.kDegrees )
        
                                            # Get the MTimes for this key and the next.
                                            cur_key_time = anim_curve_fn.time( keyframe_idx )
                                            prev_key_time = anim_curve_fn.time( keyframe_idx - 1 )
                                            
                                            # Get the difference in times.
                                            diff_time = abs( (prev_key_time.value() - cur_key_time.value()) + 1 )
                                            
                                            # Create the tangent weight.
                                            x = (diff_time * previous_keyframe.curve[2])
                                            weight = math.sqrt( angle.asRadians()*angle.asRadians() + x*x )
                                            
                                            # Set the tangent.
                                            anim_curve_fn.setTangentsLocked( keyframe_idx, False )
                                            anim_curve_fn.setWeightsLocked( keyframe_idx, False )
                                            anim_curve_fn.setTangent( keyframe_idx, angle, weight, True )
                                        
                                if tangent_out == OpenMayaAnim.MFnAnimCurve.kTangentSmooth:
                                    if keyframe_idx != len(timeline_list)-1:
                                        # Out tangent to the key (right side of the key).
                                        # This value is taken from this key's entry.
                                        for node in scale_anim_nodes:
                                            anim_curve_fn.setObject( node )
                                            
                                            # Get the values for this key and the next.
                                            cur_key_value = anim_curve_fn.value( keyframe_idx )
                                            next_key_value = anim_curve_fn.value( keyframe_idx + 1 )
                                            diff_value = abs( next_key_value - cur_key_value )
                                            
                                            # Get the percentage of the difference.
                                            diff_perc = diff_value * keyframe.curve[0]
                                            
                                            # Create the tangent angle.
                                            angle = OpenMaya.MAngle( diff_perc, OpenMaya.MAngle.kDegrees )
        
                                            # Get the MTimes for this key and the next.
                                            cur_key_time = anim_curve_fn.time( keyframe_idx )
                                            next_key_time = anim_curve_fn.time( keyframe_idx + 1 )
                                            
                                            # Get the difference in times.
                                            diff_time = abs( (next_key_time.value() - cur_key_time.value()) + 1 )
                                            
                                            # Create the tangent weight.
                                            x = (diff_time * keyframe.curve[1])
                                            weight = math.sqrt( angle.asRadians()*angle.asRadians() + x*x )
                                            
                                            # Set the tangent.                                    
                                            anim_curve_fn.setTangentsLocked( keyframe_idx, False )
                                            anim_curve_fn.setWeightsLocked( keyframe_idx, False )
                                            anim_curve_fn.setTangent( keyframe_idx, angle, weight, False )


def getKeyTangent( curve_type ):
    '''
    '''
    if curve_type is None or curve_type == 'linear':
        return OpenMayaAnim.MFnAnimCurve.kTangentLinear
    elif curve_type == 'stepped':
        return OpenMayaAnim.MFnAnimCurve.kTangentStep
    elif type(curve_type) == list:
        return OpenMayaAnim.MFnAnimCurve.kTangentSmooth


'''
============================================================
--->>   UI
============================================================
'''
def maya_main_window():
    '''
    Return the Maya main window widget as a Python object
    '''
    main_window_ptr = OpenMayaUI.MQtUtil.mainWindow()
    return wrapInstance( long( main_window_ptr ), QtGui.QWidget )
    
class BatcherWindow( QtGui.QWidget ):
    def __init__( self, parent=None ):
        super( BatcherWindow, self ).__init__( parent )
        self.setWindowTitle( 'Spine Batcher' )
        self.setWindowFlags( QtCore.Qt.Tool )
        self.setAttribute( QtCore.Qt.WA_DeleteOnClose )
        self.setParent( parent )
        self.createGUI()

    def createGUI( self ):        
        self.main_layout = QtGui.QVBoxLayout( self )
        
        self.character_name = QtGui.QLineEdit()
        self.main_layout.addWidget( self.character_name )
                
        # JSON UI.
        self.json_layout = QtGui.QHBoxLayout()
        self.json_path = QtGui.QLineEdit()
        json_button = QtGui.QPushButton()
        json_button.setText( 'Pick Spine JSON' )
        json_button.clicked.connect( self.openBrowser )
        self.json_layout.addWidget( self.json_path )
        self.json_layout.addWidget( json_button )
        self.main_layout.addLayout( self.json_layout )
        
        # Save Location UI.
        self.save_layout = QtGui.QHBoxLayout()
        self.save_path = QtGui.QLineEdit()
        save_button = QtGui.QPushButton()
        save_button.setText( 'Save Location' )
        save_button.clicked.connect( self.saveBrowser )
        self.save_layout.addWidget( self.save_path )
        self.save_layout.addWidget( save_button )
        self.main_layout.addLayout( self.save_layout )
        
        # Batch UI.        
        batch_button = QtGui.QPushButton()
        batch_button.setText( 'BATCH' )
        self.main_layout.addWidget( batch_button )
        
        self.setLayout( self.main_layout )
                
    def openBrowser( self ):
        '''
        File browser.
        
        @return: List of String. Each index is a full file path (path + name)
        '''
        browser = QtGui.QFileDialog()
        browser.setFileMode( QtGui.QFileDialog.ExistingFile )
        browser.setNameFilter( '*.json' )
        browser.setWindowTitle( 'Batch Spine JSON' )
        browser.setAcceptMode( QtGui.QFileDialog.AcceptOpen )   
        
        if browser.exec_():
            files = []
            
            for file_path in browser.selectedFiles():
                # Unicode convert to String.
                path, ext = os.path.splitext( str(file_path) )
                files.append( str(file_path) )
        
            if len(files) > 0:
                #return files
                self.json_path.setText( files[0] )
    
    def saveBrowser( self ):
        '''
        Save browser
        
        @return: List of String. Each index is a directory path.
        '''
        browser = QtGui.QFileDialog()
        browser.setFileMode( QtGui.QFileDialog.Directory )
        browser.setWindowTitle( 'Save location' )
        browser.setAcceptMode( QtGui.QFileDialog.AcceptSave )
        
        if browser.exec_():
            files = []
            
            for file_path in browser.selectedFiles():
                # Unicode convert to String.
                files.append( str(file_path) )
        
            if len(files)>0:
                #return files
                self.save_path.setText( files[0] )
                
    def doBatch( self ):
        print self.character_name
        print self.json_path
        print self.save_path
    
    
    


#json_filepath = 'E:\\Carey Project\\\Panda\\\Exports\\\BatMaleHiRes\\skeleton.json'
#json_filepath = 'E:\\Carey Project\\\Panda\\\Exports\\\PandaMaleHiRes\\skeleton.json'

json_filepath = 'E:\\Carey Project\\Panda\\exports\\MonkeyFemaleHiRes\\skeleton.json'
file_data = SpineJSON( json_filepath=json_filepath )

for key in file_data.bones.keys():
    print key

# Build the skeleton.
#buildSkeleton( file_data, OpenMaya.MDistance.kCentimeters )

#anim_name = 'CHA_ACT_flipTheBird_01'
#measurement_unit = OpenMaya.MDistance.kCentimeters
#namespace = 'target'
#applyAnimation( json_data=file_data, anim_name=anim_name, measurement_unit=measurement_unit, namespace=namespace, do_tangents=True )


